import { Platform } from '@angular/cdk/platform';
import { DOCUMENT } from '@angular/common';
import {
  Component,
  Input,
  HostBinding,
  ChangeDetectionStrategy,
  ViewEncapsulation,
  Inject,
  Output,
  EventEmitter,
  OnDestroy,
  AfterViewInit,
} from '@angular/core';

import { fromEvent, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, throttleTime } from 'rxjs/operators';

import { AnchorLinkComponent } from './anchor-link.component';

const sharpMatcherRegx = /#([^#]+)$/;

@Component({
  selector: 'fui-anchor',
  templateUrl: './anchor.component.html',
  styleUrls: ['./anchor.component.sass'],
  preserveWhitespaces: false,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnchorComponent implements AfterViewInit, OnDestroy {
  @HostBinding('class.fui-anchor') hostClass = true;

  /** The container to scroll. */
  @Input() container: Element | null = null;

  /** The scrolling behavior. */
  @Input() scrollBehavior: 'auto' | 'smooth' = 'smooth';

  /** The scrolling position of block. */
  @Input() scrollBlock: 'start' | 'center' | 'end' | 'nearest' = 'start';

  /** Emits when the link is clicked. */
  @Output() readonly clickEvent = new EventEmitter<string>();

  /** Emits when the container is scrolled. */
  @Output() readonly scrollEvent = new EventEmitter<AnchorLinkComponent>();

  private links: AnchorLinkComponent[] = [];
  private scrollSub: Subscription | null = null;
  private destroyed = false;
  private clickScrolling = false;
  private clickScrollingTimmer: NodeJS.Timeout;
  private linkSub: Subscription;
  private linkSubject = new Subject<AnchorLinkComponent>();

  constructor(
    @Inject(DOCUMENT) private _document: any,
    private platform: Platform,
  ) {
    this.linkSub = this.linkSubject.pipe(
      debounceTime(30),
    ).subscribe(() => {
      this.handleScroll();
    });
  }

  ngAfterViewInit(): void {
    this.registerScrollEvent();
  }

  ngOnDestroy(): void {
    this.destroyed = true;
    this.removeScrollListen();
    this.removeLinkListen();
    this.clearScrollTimmer();
  }

  registerLink(link: AnchorLinkComponent): void {
    this.links.push(link);
    this.linkSubject.next(link);
  }

  unregisterLink(link: AnchorLinkComponent): void {
    this.links.splice(this.links.indexOf(link), 1);
  }

  handleScrollTo(linkComponent: AnchorLinkComponent): void {
    const el = this._document.getElementById(linkComponent.href.split('#')[1]);
    if (!el) {
      return;
    }

    this.clearScrollTimmer();
    this.clickScrolling = true;

    el.scrollIntoView({ behavior: this.scrollBehavior, block: this.scrollBlock });
    this.handleActive(linkComponent);

    this.clickScrollingTimmer = setTimeout(() => {
      this.clickScrolling = false;
    }, 1000);

    this.clickEvent.emit(linkComponent.href);
  }

  private handleScroll(): void {
    // click the link to scroll will not trigger the calculating.
    if (this.clickScrolling) {
      return;
    }

    if (typeof document === 'undefined' || this.destroyed) {
      return;
    }

    // set all links unactivated.
    this.clearActive();

    const {
      containerScrollHeight,
      containerClientHeight,
      scrollRatio,
    } = this.calculateContainerScroll();

    // get all anchor elements.
    const components = this.links.filter((component) => sharpMatcherRegx.exec(component.href.toString()));
    const componentsLength = components.length;

    // active the first link when there is no scroll bar on page.
    if (containerScrollHeight === containerClientHeight && this.links.length > 0) {
      this.handleActive(this.links[0]);
      return;
    }

    for (let i = 0; i < componentsLength;  i ++) {
      const current = this._document.getElementById(components[i].href.split('#')[1]);
      let nextTop = containerScrollHeight;
      let currentTop = 0;

      if (current) {
        if (i > 0) {
          currentTop = current.offsetTop - this.getContainer().getBoundingClientRect().top;
        }
      } else {
        continue;
      }

      if (components[i + 1]) {
        const next = this._document.getElementById(components[i + 1].href.split('#')[1]);
        if (next) {
          nextTop = next.offsetTop - this.getContainer().getBoundingClientRect().top;
        }
      }

      // find the scroll interval range falling in the visible area
      if (currentTop / containerScrollHeight <= scrollRatio && nextTop / containerScrollHeight >= scrollRatio) {
        this.handleActive(this.links[i]);
        break;
      }
    }
  }

  private getContainer(): Element | null {
    return this.container || this._document;
  }

  private getContainerScrollHeight(): number {
    return this.getContainer().scrollHeight;
  }

  private getContainerScrollTop(): number {
    return this.getContainer().scrollTop;
  }

  private getContainerClientHeight(): number {
    return this.getContainer().clientHeight;
  }

  private calculateContainerScroll(): any {
    const containerScrollHeight = this.getContainerScrollHeight();
    const containerScrollTop = this.getContainerScrollTop();
    const containerClientHeight = this.getContainerClientHeight();
    return {
      containerScrollHeight,
      containerClientHeight,
      scrollRatio: containerScrollTop / (containerScrollHeight - containerClientHeight),
    };
  }

  private registerScrollEvent(): void {
    if (!this.platform.isBrowser) {
      return;
    }
    this.removeScrollListen();
    this.scrollSub = fromEvent(this.getContainer(), 'scroll')
      .pipe(throttleTime(50), distinctUntilChanged())
      .subscribe((e) => this.handleScroll());
    // Browser would maintain the scrolling position when refreshing.
    // So we have to delay calculation in avoid of getting a incorrect result.
    setTimeout(() => this.handleScroll());
  }

  private removeScrollListen(): void {
    if (this.scrollSub) {
      this.scrollSub.unsubscribe();
    }
  }

  private removeLinkListen(): void {
    if (this.linkSub) {
      this.linkSub.unsubscribe();
    }
  }

  private clearActive(): void {
    this.links.forEach((link) => {
      link.active = false;
      link.markForCheck();
    });
  }

  private handleActive(component: AnchorLinkComponent): void {
    if (component) {
      this.clearActive();

      component.active = true;
      component.markForCheck();
      this.scrollEvent.emit(component);
    }
  }

  private clearScrollTimmer(): void {
    if (this.clickScrollingTimmer) {
      clearTimeout(this.clickScrollingTimmer);
    }
  }
}
